/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE */

package au.com.lastweekend.openjaws.util;

/**
 * Once started repeatedly runs a loop until stopped.
 * 
 * <p>
 * The stop process works by interrupting the thread.
 * <p>
 * Synchronising a code block within {@link #inLoop()} will block the {@link #stop} methods.
 * <p>
 * Note, however, that the current Thread's interrupt status can be set between when the loop checks whether to continue and the
 * {@code inLoop()} method is called.
 * <p>
 * The {@link #isStopping()} method can be called within your loop to check if a stop has been requested and
 * {@code Thread.interrupted()} can be used to clear the interrupt flag.
 * <p>
 * eg.
 * 
 * <code>
 * protected void inLoop() {
 *  
 *  //OK we're going to complete this iteration now.
 *  synchronized (this) {
 *      //Clear the interrupt
 *      Thread.interrupted(); 
 *      //Do something that might block/IO etc...
 *      ... 
 *     
 *  }
 *  
 *  //Bail out now if stopping
 *  if (isStopping()) {
 *      return;
 *  }
 *  
 *  //Delay before next iteration, but allow interruption (by being outside the sync block)
 *  try {
 *      Thread.sleep(1000);
 *  } catch (InterruptedException e) {
 *      //This is expected when the stop method is called.
 *  }
 * }
 * </code>
 * 
 * 
 * @author Grant Gardner grant@lastweekend.com.au
 * @version $Id$
 */
public abstract class LoopRunnable implements Runnable {

    private volatile Thread _runner;

    /**
     * Called before the loop
     */
    protected void preLoop() {

        // do nothing;
    }

    /**
     * This method will run repeatedly until the stop method is called or the method throws an exception.
     */
    protected abstract void inLoop();

    /**
     * Called when the loop exits.
     */
    protected void stopped() {

        // do nothing;
    }

    public final void run() {

        initRunner(null);

        preLoop();

        while (!isStopping()) {
            inLoop();
            Thread.yield();
        }

        // Clear the interrupt status before calling stopped.
        Thread.interrupted();
        stopped();

    }

    private synchronized void initRunner(Thread runnerThread) throws IllegalThreadStateException {

        if (_runner == null) {
            _runner = runnerThread != null ? runnerThread : Thread.currentThread();
        }

    }

    /**
     * Only useful from the loop thread, will return false for all other threads.
     * 
     * @return whether the loop has been requested to stop.
     * @see #isRunning()
     */
    protected final synchronized boolean isStopping() {

        return _runner != Thread.currentThread() || _runner.isInterrupted();
    }

    /**
     * Meant for external threads to check status of the runner thread
     * 
     * @return live status of the runner thread
     */
    public final synchronized boolean isRunning() {

        return _runner != null && _runner.isAlive();
    }

    /**
     * Stops the loop.
     * <p>
     * See also: {@link #stopWait(long)}
     */
    public final void stop() {

        stopRunner();
    }

    /**
     * Stop the runnable and wait for its thread to die.
     * 
     * @param timeout
     *            in millis, 0 = wait forever. See {@link Thread#join(long)}
     * @throws InterruptedException
     */
    public final void stopWait(long timeout) throws InterruptedException {

        Thread tmpThread = stopRunner();
        if (tmpThread != null) {
            tmpThread.join(timeout);
        }

    }

    private synchronized Thread stopRunner() {

        Thread tmpThread = _runner;
        _runner = null;
        if (tmpThread != null) {
            tmpThread.interrupt();
        }
        return tmpThread;
    }

    /**
     * Convenience method to start this runnable in a new thread.
     * 
     * @param name
     * @return a new, started thread.
     */
    public Thread start(String name) {

        Thread newThread = new Thread(this, name);
        initRunner(newThread);
        newThread.start();
        return newThread;
    }

    /**
     * Adds a shutdown hook to stop this process when the JVM shuts down.
     */
    public final void stopAtShutdown() {

        final LoopRunnable thisLoopRunnable = this;
        Thread shutdownHook = new Thread() {

            public void run() {

                thisLoopRunnable.stop();
            }

        };
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

}